/* http.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>

#include "log.h"
#include "rio.h"
#include "http.h"

#define URIHEADER       "http://"
#define DFLPAGE         "index.html"

#define ARRAY_SIZE(a)   (sizeof(a)/sizeof(a[0]))
#define CGIMODE         0700
#define MAXENVS         4096 
#define MAXLINE         1024

extern char **environ;          /* environment pointer */
extern char webdir[];           /* path of the tiny directory */
extern char cgidir[];           /* path of the cgi directory */
extern int  port;               /* connection port */
static char *newenv[MAXENVS];   /* new environment array */
static int  envcount;           /* current envrionment count */

typedef void (*func)(http_request *, http_response *);
static void do_get(http_request *reqeust, http_response *response);
static void do_post(http_request *request, http_response *response);

static struct {
    char    *method;
    func    function;
} method_functions[] = {
    {"GET",     do_get},
    {"POST",    do_post},
    {"HEAD",    do_get},
    {NULL,      NULL},
};

static struct {
    char *extension;
    char *mimetype;
} extension_mimetypes[] = {
    {".txt",	"text/plain"},
    {".html",	"text/html"},
    {".htm",	"text/html"},
    {".rtf",	"ext/rtf"},
    {".cgm",	"image/cgm"},
    {".gif",	"image/gif"},
    {".ief",	"image/ief"},
    {".jpg",	"image/jpg"},
    {".jpeg",	"image/jpg"},
    {".png",	"image/png"},
    {".tif",	"image/tiff"},
    {".tiff",	"image/tiff"},
    {".ps",		"application/postscript"},
    {".ai",		"application/postscript"},	
    {".eps",	"application/postscript"},
    {".pdf",	"application/pdf"},
    {".xls",	"application/vnd.ms-excel"},
    {".xlt",	"application/vnd.ms-excel"},
    {".doc",	"application/msword"},
    {".wpd",	"application/wordperfect5.1"},
    {".wp6",	"application/wordperfect5.1"},
    {".ppt",	"application/vnd.ms-powerpoint"},
    {".pot",	"application/vnd.ms-powerpoint"},
    {".pps",	"application/vnd.ms-powerpoint"},
    {".tex",	"application/x-tex"},
    {".latex",	"application/x-latex"},
    {".dvi",	"application/x-dvi"},
    {".tar.Z",	"application/x-compressed-tar"},
    {".tar.bz2","application/x-bzip-compressed-tar"},
    {".zip",	"application/zip"},
    {".tar",	"application/x-tar"},
    {".gz",		"application/x-gzip"},
    {".gzip",	"application/x-gzip"},
    {".bz",		"application/x-bzip"},
    {".bz2",	"application/x-bzip2"},
    {".bzip2",	"application/x-bzip2"},
    {".jar",	"application/java-archive"},
    {".shar",	"application/x-shar"},
    {".rpm",	"application/x-rpm"},
    {".au",		"audio/basic"},
    {".mpg",	"video/mpeg"},
    {".mpeg",	"video/mpeg"},
    {".mp3",	"audio/mp3"},
    {".qt",		"video/quicktime"},
    {".ra",		"audio/x-pn-realaudio"},
    {".ram",	"audio/x-pn-realaudio"},
    {NULL,      "application/octet-stream"}
};

static struct {
    int     errnum;
    char    *msg;
} errnum_msgs[] = {
    {100, "Continue"},
    {101, "Switching Protocols"},
    {200, "OK"},
    {201, "Created"},
    {202, "Accepted"},
    {203, "Non-Authoritative Information"},
    {204, "No Content"},
    {205, "Reset Content"},
    {206, "Partial Content"},
    {300, "Multiple Choices"},
    {301, "Moved Permanently"},
    {302, "Found"},
    {303, "See Other"},
    {304, "Not Modified"},
    {305, "Use Proxy"},
    {307, "Temporary Redirect"},
    {400, "Bad Request"},
    {401, "Unauthorized"},
    {402, "Payment Required"},
    {403, "Forbidden"},
    {404, "Not Found"},
    {405, "Method Not Allowed"},
    {406, "Not Acceptable"},
    {407, "Proxy Authentication Required"},
    {408, "Request Timeout"},
    {409, "Conflict"},
    {410, "Gone"},
    {411, "Length Required"},
    {412, "Precondition Failed"},
    {413, "Request Entity Too Large"},
    {414, "Request_URI Too Long"},
    {415, "Unsupported Media Type"},
    {416, "Requested Range Not Satisfiable"},
    {417, "Expectation Failed"},
    {500, "Internal Server Error"},
    {501, "Not Implemented"},
    {502, "Bad Gateway"},
    {503, "Service Unavailable"},
    {504, "Gateway Timeout"},
    {505, "HTTP Version Not Supported"},
    {0,   "Unkown error number"}
};

static int copy_env()
{
    for (envcount = 0; environ[envcount]; envcount++) {
        if (envcount >= MAXENVS)
            return -1;
        newenv[envcount] = environ[envcount];
    }
    return 0;
}

static int add_env(const char *fmt, ...)
{
    char    buf[MAXLINE], *p;
    va_list ap;
    
    if (envcount >= MAXENVS)
        return -1;
    
    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    p = strdup(buf);
    if (p == NULL)
        return -1;

    newenv[envcount++] = p;

    return 0;
}

static void set_env()
{
    newenv[envcount] = NULL;
    environ = newenv;
}

/* read headers in a line */
static int read_headers(http_request *hp, rio_t *rp)
{
    int n;

    errno = 0;
    if ((n = rio_readlineb(rp, hp->buf+hp->buf_offset, 
                MAXBUF-hp->buf_offset)) >= 0) {
        hp->buf_offset += n;
        if (hp->buf[hp->buf_offset-1] == '\n')
            return 1;
        else
            return 0;
    }

    return -1;
}

/* read content for size bytes */
static int read_content(http_request *hp, rio_t *rp)
{
    int n;

    if (hp->content_len > MAXBUF) {
        log_error("content length is more than %d", MAXBUF);
        return -1;
    }
    
    errno = 0;
    if ((n = rio_readnb(rp, hp->buf+hp->buf_offset, 
                hp->content_len-hp->buf_offset)) >= 0) {
        hp->buf_offset += n;
        if (hp->content_len == hp->buf_offset)
            return 1;
        else
            return 0;
    }
    
    return -1;
}

/* parse the uri return 1 if request static content */
static int http_parseuri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;

    /* delete the header of the uri */
    if (strncmp(uri, URIHEADER, sizeof(URIHEADER)-1) == 0) {
        uri += sizeof(URIHEADER);
        uri = index(uri, '/');
    }

    /* static content */
    if (strstr(uri, cgidir) == 0) {
        strcpy(cgiargs, ""); 
        strcpy(filename, webdir);
        strcat(filename, uri);
        if (uri[strlen(uri)-1] == '/')
            strcat(filename, DFLPAGE);
        return 1;
    }

    /* dynamic content */
    else {
        ptr = index(uri, '?'); 
        if (ptr != NULL) {
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        }
        else
            strcpy(cgiargs, "");
        strcpy(filename, webdir);
        strcat(filename, uri);
        return 0;
    }
}

char *makehttpdate(time_t when, char *buf)
{
    struct tm gm_tm;

    gm_tm = *gmtime(&when);
    strftime(buf, 64, "%a, %d %b %Y %H:%M:%S GMT", &gm_tm);

    return buf;
}

static func getfunction(char *method)
{
    int i;

    for (i = 0; method_functions[i].method != NULL; i++) {
        if (strcasecmp(method_functions[i].method, method) == 0)
            break;
    }
    
    return method_functions[i].function;
}

static char *getmimetype(char *filename)
{
    int     i;
    char    *extension = rindex(filename, '.');

    for (i = 0; extension_mimetypes[i].extension != NULL; i++) {
        if (strcasecmp(extension, extension_mimetypes[i].extension) == 0)
            break;
    }

    return extension_mimetypes[i].mimetype;
}

static char *geterrmsg(int errnum)
{
    int i;

    for (i = 0; errnum_msgs[i].errnum != 0; i++) {
        if (errnum_msgs[i].errnum == errnum)
            break;
    }

    return errnum_msgs[i].msg;
}

static void clienterror(http_response *response, char *cause, int errnum)
{
    int len;
    char body[MAXBUF], *msg = geterrmsg(errnum);

    /* clear the response buffer */
    memset(response->buf, 0, MAXBUF);
    response->size = 0;

    /* build the HTTP reponse body */
    sprintf(body, "<html><title>Tiny Error</title>");
    sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
    sprintf(body, "%s%d: %s\r\n", body, errnum, msg);
    sprintf(body, "%s<p>%s\r\n", body, cause);
    sprintf(body, "%s<hr><em>Tiny Web Server</em></body></html>\r\n", body);
    len = strlen(body);

    /* build the HTTP response header */
    sprintf(response->buf, "HTTP/1.0 %d %s\r\n", errnum, msg);
    sprintf(response->buf, "%sContent-Type: text/html\r\n", response->buf);
    sprintf(response->buf, "%sContent-Length: %d\r\n\r\n", response->buf,
        len);
    response->size = strlen(response->buf);
    memcpy(response->buf+response->size, body, len);
    response->size += len;
}

static void sendfile(http_request *request, http_response *response, 
    char *filename, int filesize, int status)
{
    int     srcfd, save_err;
    char    *srcp, *mimetype;

    /* clear the response buffer */
    memset(response->buf, 0, response->size);
    response->size = 0;

    /* open the file */
    srcfd = open(filename, O_RDONLY);
    if (srcfd < 0) {
        save_err = errno;
        log_error("open %s error: %s", filename, strerror(errno));
        switch(save_err) {
        case EACCES:
            clienterror(response, filename, 403);
            return ;
        case ENOTDIR:
        case ENOENT:
            clienterror(response, filename, 404);
            return ;
        case EMFILE:
        case ENOMEM:
        case EAGAIN:
        case ETXTBSY:
            clienterror(response, filename, 503);
            return ;
        case ELOOP:
        case ENAMETOOLONG:
        default:
            clienterror(response, filename, 500);
            return ;
        }
    }

    /* send response header */
    mimetype = getmimetype(filename);
    sprintf(response->buf, "HTTP/1.0 %d %s\r\n", status, geterrmsg(status));
    sprintf(response->buf, "%sServer: Tiny Web Server\r\n", response->buf);
    sprintf(response->buf, "%sContent-length: %d\r\n", response->buf,
        filesize);
    sprintf(response->buf, "%sContent-type: %s\r\n\r\n", response->buf,
        mimetype);
    response->size = strlen(response->buf);
     
    /* for method HEAD */
    if (strcasecmp(request->method, "HEAD") == 0)
        return;

    /* send response body */
    srcp = mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    close(srcfd);
    memcpy(response->buf+response->size, srcp, filesize);
    response->size += filesize;
    munmap(srcp, filesize);
}

static void serve_static(http_request *request, http_response *response, 
    char *filename, int filesize)
{
    /* send file at status 200 */
    sendfile(request, response, filename, filesize, 200);
}

static void serve_dynamic(http_request *request, http_response *response, 
    char *filename)
{
    int         fd[2], status;
    char        *emptylist[] = {NULL}, buf[MAXBUF], file[MAXFILE], *p, *q;
    struct stat sbuf;
    
    if (pipe(fd) < 0) {
        log_error("pipe error: %s", strerror(errno));
        clienterror(response, "pipe error", 500); 
        return ;
    }

    if (fork() == 0) {
        close(fd[0]);

        /* set the environ */
        copy_env();
        gethostname(buf, MAXBUF);
        add_env("SERVE_NAME=%s", buf);
        add_env("SERVER_PORT=%d", port);
        add_env("SERVER_PROTOCOL=HTTP/1.0");
        add_env("SCRIPT_FILENAME=%s", filename);
        add_env("REQUEST_METHOD=%s", request->method);
        add_env("REQUEST_URI=%s", request->uri);
        add_env("REMOTE_HOST=%s", request->remote_name);
        add_env("REMOTE_ADDR=%s", request->remote_addr);
        add_env("CONTENT_TYPE=%s", request->content_type);
        add_env("CONTENT_LENGTH=%d", request->content_len);
        add_env("QUERY_STRING=%s", request->buf);    
        set_env();

        dup2(fd[1], STDOUT_FILENO);
        if (execve(filename, emptylist, newenv) < 0) {
            printf("Status: 500\n");
            printf("Content-Type: text/plain\n");
            printf("\n");
            printf("execve(%s) error: %s\n", filename, strerror(errno));
            exit(0);
        }
    }
    else {
        close(fd[1]); 

        memset(buf, 0, MAXBUF);
        rio_readn(fd[0], buf, MAXBUF);
        log_info("cgi output is %s", buf);
        wait(NULL);

        /* clear the response buffer */
        memset(response->buf, 0, response->size);
        response->size = 0;

        /* send response header */
        sprintf(response->buf, "HTTP/1.0 200 OK\r\n");
        sprintf(response->buf, "%sServer: Tiny Web Server\r\n", 
            response->buf);
        response->size = strlen(response->buf);

        p = buf;

        /* parse the first line containing Status: xxx */
        if (strncasecmp(buf, "Status:", 7) == 0) {
            /* get the first line of the response */
            q = index(p, '\n');
            if (q == NULL) {
                clienterror(response, p, 502); 
                close(fd[0]);
                return ;
            }
            *q = '\0';

            status = strtol(buf+8, NULL, 0); 
            if (status != 200) {
                memset(response->buf, 0, response->size);
                response->size = 0;
                sprintf(response->buf, "HTTP/1.0 %d %s\r\n", status, 
                    geterrmsg(status));
                sprintf(response->buf, "%sServer: Tiny Web Server\r\n", 
                    response->buf);
                response->size = strlen(response->buf);
            }    
            p = q + 1;
        }
        else if (strncasecmp(buf, "Location:", 9) == 0) {
            /* get the first line of the response */
            q = index(p, '\n');
            if (q == NULL) {
                clienterror(response, p, 502); 
                close(fd[0]);
                return ;
            }
            *q = '\0';

            /* get the redirected file location */
            strcpy(file, webdir);
            strcat(file, "/");
            strcat(file, buf+10);

            /* get the file size */
            if (stat(filename, &sbuf) < 0) {
                clienterror(response, filename, 404);
                return ;
            }
            
            /* send file at status 301 */
            sendfile(request, response, filename, sbuf.st_size, 301);
            close(fd[0]);
            return ; 
        }

        /* for method HEAD */
        if (strcasecmp(request->method, "HEAD") == 0) {
            close(fd[0]);
            return;
        }

        /* send response body */
        strncpy(response->buf+response->size, p, MAXBUF-response->size);
        response->size = strlen(response->buf);
        close(fd[0]);
    }
}

static void do_get(http_request *request, http_response *response)
{
    int is_static;
    struct stat sbuf;
    char filename[MAXFILE];

    /* Parse URI from request */
    is_static = http_parseuri(request->uri, filename, request->buf);
    if (stat(filename, &sbuf) < 0) {
        clienterror(response, filename, 404);
        return ;
    }

    /* server static content */
    if (is_static) {    
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
            clienterror(response, filename, 403);
            return ;
        }
        serve_static(request, response, filename, sbuf.st_size);
    } 

    /* server dynamic content */
    else {              
        if (!(S_ISREG(sbuf.st_mode)) || !(CGIMODE & sbuf.st_mode)) {
            clienterror(response, filename, 403);
            return ;
        }
        serve_dynamic(request, response, filename);
    }
}

static void do_post(http_request *request, http_response *response)
{
    struct stat sbuf;
    char filename[MAXFILE], cgiargs[MAXCGI];

    http_parseuri(request->uri, filename, cgiargs);
    if (stat(filename, &sbuf) < 0) {
        clienterror(response, filename, 404);
        return ;
    }

    if (!(S_ISREG(sbuf.st_mode)) || !(CGIMODE & sbuf.st_mode)) {
        clienterror(response, filename, 403);
        return ;
    }

    if (request->content_len <= 0) {
        clienterror(response, "content length", 400);
        return ;
    }

    serve_dynamic(request, response, filename);
}

/* if completed then return 1, not completed return 0, error return -1 */
int http_readrequest(http_request *hp, rio_t *rp)
{
    int ret;

    /* get method, uri, and version */
    if (hp->method[0] == '\0') {
        if ((ret = read_headers(hp, rp)) == 1) {
            sscanf(hp->buf, "%s %s %s", hp->method, hp->uri, hp->version);
            memset(hp->buf, 0, sizeof(hp->buf));
            hp->buf_offset = 0;
        }
        else
            return ret;
    }

    /* read other headers */
    if (hp->request_over == 0) {
        while ((ret = read_headers(hp, rp)) == 1) {
            if (strncasecmp(hp->buf, "Content-Type:", 13) == 0)
                strcpy(hp->content_type, hp->buf+13);
            else if (strncasecmp(hp->buf, "Content-Length:", 15) == 0)
                hp->content_len = atoi(&hp->buf[16]);
            else if (strncmp(hp->buf, "\r\n", 3) == 0) {
                hp->request_over = 1;
                memset(hp->buf, 0, sizeof(hp->buf));
                hp->buf_offset = 0;
                break;
            }

            memset(hp->buf, 0, sizeof(hp->buf));
            hp->buf_offset = 0;
        }
        
        if (hp->request_over == 0)
            return ret;
    }

    /* read content */
    if (hp->content_len > 0)
        return read_content(hp, rp);

    return 1;
}

/* if completed then return 1, not completed return 0, error return -1 */
int http_writeresponse(http_response *hp, int fd)
{
    int n;
    
    while ((n = write(fd, hp->buf+hp->offset, 
                hp->size-hp->offset)) < 0) {
        if (errno == EAGAIN)
            return 0;
        else if (errno != EINTR)
            return -1;
    }

    hp->offset += n;
    if (hp->offset != hp->size)
        return 0;
    else
        return 1;
}

void http_serve(http_request *request, http_response *response)
{
    func function;

    /* find the method and handle request with appropriate function */
    if ((function = getfunction(request->method)) != NULL)
        function(request, response);
    else 
        clienterror(response, request->method, 501);
}
